package org.etnaframework.module.base.spring;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.DigestUtils;
import org.springframework.web.context.support.RequestHandledEvent;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.util.*;
import java.util.Map.Entry;

/**
 * 用于全局管理Spring的ApplicationContext
 * <p>
 * 其中有部分是从{@link ApplicationContext}代理的方法，加了初始化完成的判断，减少代码错误
 *
 * @author jasyaf
 * @since 2023-12-07
 */
public class SpringContext {

    private static final Logger log = LoggerFactory.getLogger(SpringContext.class);

    private static ApplicationContext CONTEXT;

    private static volatile Date STARTED_TIME = null;

    private static volatile String SPRING_BOOT_JAR_MD5 = null;

    private static final Map<String, Object> extraConfigMap = new LinkedHashMap<>();

    private SpringContext() {
    }

    public static void setApplicationContext(ApplicationContext applicationContext) {
        if (null == CONTEXT) {
            CONTEXT = applicationContext;
        }
    }

    private static void _checkContextInitialized() {
        if (null == CONTEXT) {
            String msg = SpringContext.class.getSimpleName() + " is initializing, DO NOT invoke .getBeanXXX method";
            throw new RuntimeException(msg);
        }
    }

    /**
     * 返回当前服务【成功完成初始化】的时间点
     * <p>
     * 注意是完成Spring和业务功能的初始化之后的时间点，不是java进程的启动时间
     * 在初始化成功之前（包含初始化出错失败的情况）调用本方法，返回值为null
     */
    public static Date getStartedTime() {
        return STARTED_TIME;
    }

    /**
     * 返回当前服务启动jar包的MD5值，如果当前不是通过jar包方式启动的就返回null
     */
    public static String getSpringBootJarMD5() {
        return SPRING_BOOT_JAR_MD5;
    }

    /**
     * 获取满足指定类型的bean的列表
     */
    public static <T> List<T> getBeansOfTypeAsList(Class<T> type) throws BeansException {
        _checkContextInitialized();
        return new ArrayList<>(CONTEXT.getBeansOfType(type, false, true).values());
    }

    /**
     * 获取满足指定类型的bean，如果有多个会抛出异常，如果没有会返回null
     */
    public static <T> T getBeanOfType(Class<T> type) throws BeansException {
        _checkContextInitialized();
        Map<String, T> map = getBeansOfType(type);
        if (!map.isEmpty()) { // 如果有实现类，就注入，否则就留null
            if (map.size() > 1) {
                throw new IllegalStateException(type.getSimpleName() + " has more than one instance");
            }
            return map.values().iterator().next();
        }
        return null;
    }

    /**
     * 添加额外的key:value对，用于在最后启动成功时展示出来（例如启动了额外的服务，把端口显示出来）
     */
    public static void addExtraConfig(String key, Object value) {
        extraConfigMap.put(key, String.valueOf(value));
    }

    /**
     * 服务器启动入口，针对Spring原来的做了优化
     */
    public static void start(Class<?> primarySource, String... args) {
        long startMs = System.currentTimeMillis();
        try {
            boolean jarMode = primarySource.getClassLoader().getClass().getName().equals("org.springframework.boot.loader.LaunchedURLClassLoader");
            // 额外检查：如果是在IDE里面启动（main方法启动），就检查一下当前运行目录下面有没有target文件夹
            // 如果没有就说明Working directory肯定不在当前项目module下（当然，如果有也不能说明一定是，但没有一定就不是）
            // 直接提示错误并给出配置指引。这样做的目的，是当项目中需要读取额外的资源时（证书或数据库文件等）
            // 不同的开发者在本机调试时能有一个统一的起始工作目录，通过相对路径去加载资源（因为每个开发放置项目文件的目录都不一样就没办法用绝对路径）
            // 如果都按下列的规则统一设置了，例如证书文件就放在module下的config/test.cer，大家本机调试就都能通过相对路径config/test.cer读取到
            if (!jarMode) {
                File targetDir = new File(System.getProperty("user.dir") + File.separator + "target");
                if (!targetDir.exists() || !targetDir.isDirectory()) {
                    StringJoiner joiner = new StringJoiner("\n");
                    joiner.add("");
                    joiner.add("==============================================================================================================");
                    joiner.add("If you're using IntelliJ IDEA to run the program, you need to set the working directory to load the resource");
                    joiner.add("Make sure Menu [Run] -> [Edit Configurations] -> [Build and run] -> [Working directory] is $MODULE_DIR$");
                    joiner.add("If absent, click [Modify options] on the right side of [Build and run] and add [Working directory]");
                    joiner.add("");
                    joiner.add("Working directory: " + new File(System.getProperty("user.dir")));
                    joiner.add("==============================================================================================================");
                    throw new IllegalArgumentException(joiner.toString());
                }
            } else {
                String cmd = System.getProperty("sun.java.command");
                String jar = cmd.split(" ")[0];
                File jarFile = new File(jar);
                if (jarFile.exists() && jarFile.isFile()) {
                    SPRING_BOOT_JAR_MD5 = DigestUtils.md5DigestAsHex(Files.newInputStream(jarFile.toPath()));
                }
            }

            // 默认方式启动时，当启动成功时会输出 Started XXX (XXX为启动类名) in XX seconds 的文字内容
            // 可以使用脚本据此判断服务有没有启动成功
            // 但是看源码可以发现一个问题
            // org.springframework.boot.SpringApplication.run(java.lang.String...)
            // 实际上 CommandLineRunner 这些是在打印启动成功之后才执行的
            // 也就是说，会存在这样的场景，显示了启动成功，但由于后续任务执行失败导致进程关闭
            // 为了避免这种情况，调用.logStartupInfo(false)关闭自带的显示，然后再写一个类似的，在后续任务执行完后再打印
            new SpringApplicationBuilder(primarySource).logStartupInfo(false).run(args);
            StringBuilder sb = new StringBuilder("\n");
            sb.append("============================================================\n");
            Map<String, Object> configMap = new LinkedHashMap<>();
            configMap.put("java.vm.name", System.getProperty("java.vm.name"));
            configMap.put("java.version", System.getProperty("java.version"));
            configMap.put("java.pid-host", ManagementFactory.getRuntimeMXBean().getName());
            if (!jarMode) { // 通过java -jar方式启动（生产环境）还是直接通过main方法启动（本机调试）
                configMap.put("java.launcher", "main");
            } else {
                if (null != SPRING_BOOT_JAR_MD5) {
                    configMap.put("java.launcher", "jar-" + SPRING_BOOT_JAR_MD5);
                } else {
                    configMap.put("java.launcher", "jar");
                }
            }
            configMap.put("def.locale", Locale.getDefault().toString());
            configMap.put("def.timezone", TimeZone.getDefault().getID());
            configMap.put("user.dir", System.getProperty("user.dir"));

            ServerProperties serverProperties = CONTEXT.getBean(ServerProperties.class);
            if (null == serverProperties.getPort()) {
                throw new IllegalArgumentException("server.port is invalid");
            } else {
                configMap.put("server.port", serverProperties.getPort());
            }
            configMap.putAll(extraConfigMap);
            int maxKeyLength = 0;
            for (String k : configMap.keySet()) {
                maxKeyLength = Math.max(k.length(), maxKeyLength);
            }
            for (Entry<String, Object> e : configMap.entrySet()) {
                sb.append(String.format("%-" + maxKeyLength + "s = %s", e.getKey(), e.getValue())).append("\n");
            }
            sb.append("============================================================\n");
            long now = System.currentTimeMillis();
            sb.append("Started ").append(primarySource.getSimpleName()).append(" in ").append((now - startMs) / 1000.0).append(" seconds");
            log.info(sb.toString());
            STARTED_TIME = new Date(now);
        } catch (Throwable ex) {
            log.error("{} EXITED", primarySource.getSimpleName(), ex);
            System.exit(1); // 如果业务代码在初始化时启用了守护线程，则打印错误日志后进程并不会自动退出，会一直卡着僵死，故这里再调用一次退出
        }
    }

    /**
     * Return the number of beans defined in the factory.
     * <p>
     * Does not consider any hierarchy this factory may participate in, and ignores any singleton beans that have been registered by other means than bean definitions.
     *
     * @return the number of beans defined in the factory
     */
    public static int getBeanDefinitionCount() {
        _checkContextInitialized();
        return CONTEXT.getBeanDefinitionCount();
    }

    /**
     * Return the names of all beans defined in this factory.
     * <p>
     * Does not consider any hierarchy this factory may participate in, and ignores any singleton beans that have been registered by other means than bean definitions.
     *
     * @return the names of all beans defined in this factory, or an empty array if none defined
     */
    public static String[] getBeanDefinitionNames() {
        _checkContextInitialized();
        return CONTEXT.getBeanDefinitionNames();
    }

    /**
     * Return the names of beans matching the given type (including subclasses), judging from either bean definitions or the value of {@code getObjectType} in the case of FactoryBeans.
     * <p>
     * <b>NOTE: This method introspects top-level beans only.</b> It does <i>not</i> check nested beans which might match the specified type as well.
     * <p>
     * Does consider objects created by FactoryBeans, which means that FactoryBeans will get initialized. If the object created by the FactoryBean doesn't match, the raw FactoryBean itself will be
     * matched against the type.
     * <p>
     * Does not consider any hierarchy this factory may participate in. Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} to include beans in ancestor factories too.
     * <p>
     * Note: Does <i>not</i> ignore singleton beans that have been registered by other means than bean definitions.
     * <p>
     * This version of {@code getBeanNamesForType} matches all kinds of beans, be it singletons, prototypes, or FactoryBeans. In most implementations, the result will be the same as for
     * {@code getBeanNamesForType(type, true, true)}.
     * <p>
     * Bean names returned by this method should always return bean names <i>in the order of definition</i> in the backend configuration, as far as possible.
     *
     * @param type the class or interface to match, or {@code null} for all bean names
     * @return the names of beans (or objects created by FactoryBeans) matching the given object type (including subclasses), or an empty array if none
     * @see FactoryBean#getObjectType
     * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, Class)
     */
    public static String[] getBeanNamesForType(Class<?> type) {
        _checkContextInitialized();
        return CONTEXT.getBeanNamesForType(type);
    }

    /**
     * Return the names of beans matching the given type (including subclasses), judging from either bean definitions or the value of {@code getObjectType} in the case of FactoryBeans.
     * <p>
     * <b>NOTE: This method introspects top-level beans only.</b> It does <i>not</i> check nested beans which might match the specified type as well.
     * <p>
     * Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, which means that FactoryBeans will get initialized. If the object created by the FactoryBean doesn't match,
     * the raw FactoryBean itself will be matched against the type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked (which doesn't require initialization of each FactoryBean).
     * <p>
     * Does not consider any hierarchy this factory may participate in. Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} to include beans in ancestor factories too.
     * <p>
     * Note: Does <i>not</i> ignore singleton beans that have been registered by other means than bean definitions.
     * <p>
     * Bean names returned by this method should always return bean names <i>in the order of definition</i> in the backend configuration, as far as possible.
     *
     * @param type                 the class or interface to match, or {@code null} for all bean names
     * @param includeNonSingletons whether to include prototype or scoped beans too or just singletons (also applies to FactoryBeans)
     * @param allowEagerInit       whether to initialize <i>lazy-init singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods with a "factory-bean" reference) for the type check.
     *                             Note that FactoryBeans need to be eagerly initialized to determine their type: So be aware that passing in "true" for this flag will initialize FactoryBeans and "factory-bean"
     *                             references.
     * @return the names of beans (or objects created by FactoryBeans) matching the given object type (including subclasses), or an empty array if none
     * @see FactoryBean#getObjectType
     * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean)
     */
    public static String[] getBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
        _checkContextInitialized();
        return CONTEXT.getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
    }

    /**
     * Return an instance, which may be shared or independent, of the specified bean.
     * <p>
     * This method allows a Spring BeanFactory to be used as a replacement for the Singleton or Prototype design pattern. Callers may retain references to returned objects in the case of Singleton
     * beans.
     * <p>
     * Translates aliases back to the corresponding canonical bean name. Will ask the parent factory if the bean cannot be found in this factory instance.
     *
     * @param name the name of the bean to retrieve
     * @return an instance of the bean
     * @throws NoSuchBeanDefinitionException if there is no bean definition with the specified name
     * @throws BeansException                if the bean could not be obtained
     */
    public static Object getBean(String name) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBean(name);
    }

    /**
     * Return an instance, which may be shared or independent, of the specified bean.
     * <p>
     * Behaves the same as {@link #getBean(String)}, but provides a measure of type safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the required type. This means that
     * ClassCastException can't be thrown on casting the result correctly, as can happen with {@link #getBean(String)}.
     * <p>
     * Translates aliases back to the corresponding canonical bean name. Will ask the parent factory if the bean cannot be found in this factory instance.
     *
     * @param name         the name of the bean to retrieve
     * @param requiredType type the bean must match. Can be an interface or superclass of the actual class, or {@code null} for any match. For example, if the value is {@code Object.class}, this
     *                     method will succeed whatever the class of the returned instance.
     * @return an instance of the bean
     * @throws NoSuchBeanDefinitionException  if there is no such bean definition
     * @throws BeanNotOfRequiredTypeException if the bean is not of the required type
     * @throws BeansException                 if the bean could not be created
     */
    public static <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBean(name, requiredType);
    }

    /**
     * Return the bean instances that match the given object type (including subclasses), judging from either bean definitions or the value of {@code getObjectType} in the case of FactoryBeans.
     * <p>
     * <b>NOTE: This method introspects top-level beans only.</b> It does <i>not</i> check nested beans which might match the specified type as well.
     * <p>
     * Does consider objects created by FactoryBeans, which means that FactoryBeans will get initialized. If the object created by the FactoryBean doesn't match, the raw FactoryBean itself will be
     * matched against the type.
     * <p>
     * Does not consider any hierarchy this factory may participate in. Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} to include beans in ancestor factories too.
     * <p>
     * Note: Does <i>not</i> ignore singleton beans that have been registered by other means than bean definitions.
     * <p>
     * This version of getBeansOfType matches all kinds of beans, be it singletons, prototypes, or FactoryBeans. In most implementations, the result will be the same as for
     * {@code getBeansOfType(type, true, true)}.
     * <p>
     * The Map returned by this method should always return bean names and corresponding bean instances <i>in the order of definition</i> in the backend configuration, as far as possible.
     *
     * @param type the class or interface to match, or {@code null} for all concrete beans
     * @return a Map with the matching beans, containing the bean names as keys and the corresponding bean instances as values
     * @throws BeansException if a bean could not be created
     * @see FactoryBean#getObjectType
     * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class)
     * @since 1.1.2
     */
    public static <T> Map<String, T> getBeansOfType(Class<T> type) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBeansOfType(type);
    }

    /**
     * Return the bean instance that uniquely matches the given object type, if any.
     *
     * @param requiredType type the bean must match; can be an interface or superclass. {@code null} is disallowed.
     *                     <p>
     *                     This method goes into {@link ListableBeanFactory} by-type lookup territory but may also be translated into a conventional by-name lookup based on the name of the given type. For more
     *                     extensive retrieval operations across sets of beans, use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}.
     * @return an instance of the single bean matching the required type
     * @throws NoSuchBeanDefinitionException   if no bean of the given type was found
     * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
     * @see ListableBeanFactory
     * @since 3.0
     */
    public static <T> T getBean(Class<T> requiredType) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBean(requiredType);
    }

    /**
     * Return an instance, which may be shared or independent, of the specified bean.
     * <p>
     * Allows for specifying explicit constructor arguments / factory method arguments, overriding the specified default arguments (if any) in the bean definition.
     *
     * @param name the name of the bean to retrieve
     * @param args arguments to use when creating a bean instance using explicit arguments (only applied when creating a new instance as opposed to retrieving an existing one)
     * @return an instance of the bean
     * @throws NoSuchBeanDefinitionException if there is no such bean definition
     * @throws BeanDefinitionStoreException  if arguments have been given but the affected bean isn't a prototype
     * @throws BeansException                if the bean could not be created
     * @since 2.5
     */
    public static Object getBean(String name, Object... args) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBean(name, args);
    }

    /**
     * Return the bean instances that match the given object type (including subclasses), judging from either bean definitions or the value of {@code getObjectType} in the case of FactoryBeans.
     * <p>
     * <b>NOTE: This method introspects top-level beans only.</b> It does <i>not</i> check nested beans which might match the specified type as well.
     * <p>
     * Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, which means that FactoryBeans will get initialized. If the object created by the FactoryBean doesn't match,
     * the raw FactoryBean itself will be matched against the type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked (which doesn't require initialization of each FactoryBean).
     * <p>
     * Does not consider any hierarchy this factory may participate in. Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} to include beans in ancestor factories too.
     * <p>
     * Note: Does <i>not</i> ignore singleton beans that have been registered by other means than bean definitions.
     * <p>
     * The Map returned by this method should always return bean names and corresponding bean instances <i>in the order of definition</i> in the backend configuration, as far as possible.
     *
     * @param type                 the class or interface to match, or {@code null} for all concrete beans
     * @param includeNonSingletons whether to include prototype or scoped beans too or just singletons (also applies to FactoryBeans)
     * @param allowEagerInit       whether to initialize <i>lazy-init singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods with a "factory-bean" reference) for the type check.
     *                             Note that FactoryBeans need to be eagerly initialized to determine their type: So be aware that passing in "true" for this flag will initialize FactoryBeans and "factory-bean"
     *                             references.
     * @return a Map with the matching beans, containing the bean names as keys and the corresponding bean instances as values
     * @throws BeansException if a bean could not be created
     * @see FactoryBean#getObjectType
     * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean)
     */
    public static <T> Map<String, T> getBeansOfType(Class<T> type, boolean includeNonSingletons, boolean allowEagerInit) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBeansOfType(type, includeNonSingletons, allowEagerInit);
    }

    /**
     * Return an instance, which may be shared or independent, of the specified bean.
     * <p>
     * Allows for specifying explicit constructor arguments / factory method arguments, overriding the specified default arguments (if any) in the bean definition.
     *
     * @param requiredType type the bean must match; can be an interface or superclass. {@code null} is disallowed.
     *                     <p>
     *                     This method goes into {@link ListableBeanFactory} by-type lookup territory but may also be translated into a conventional by-name lookup based on the name of the given type. For more
     *                     extensive retrieval operations across sets of beans, use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}.
     * @param args         arguments to use when creating a bean instance using explicit arguments (only applied when creating a new instance as opposed to retrieving an existing one)
     * @return an instance of the bean
     * @throws NoSuchBeanDefinitionException if there is no such bean definition
     * @throws BeanDefinitionStoreException  if arguments have been given but the affected bean isn't a prototype
     * @throws BeansException                if the bean could not be created
     * @since 4.1
     */
    public static <T> T getBean(Class<T> requiredType, Object... args) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBean(requiredType, args);
    }

    /**
     * Find all names of beans whose {@code Class} has the supplied {@link Annotation} type, without creating any bean instances yet.
     *
     * @param annotationType the type of annotation to look for
     * @return the names of all matching beans
     * @since 4.0
     */
    public static String[] getBeanNamesForAnnotation(Class<? extends Annotation> annotationType) {
        _checkContextInitialized();
        return CONTEXT.getBeanNamesForAnnotation(annotationType);
    }

    /**
     * Find all beans whose {@code Class} has the supplied {@link Annotation} type, returning a Map of bean names with corresponding bean instances.
     *
     * @param annotationType the type of annotation to look for
     * @return a Map with the matching beans, containing the bean names as keys and the corresponding bean instances as values
     * @throws BeansException if a bean could not be created
     * @since 3.0
     */
    public static Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> annotationType) throws BeansException {
        _checkContextInitialized();
        return CONTEXT.getBeansWithAnnotation(annotationType);
    }

    /**
     * Notify all listeners registered with this application of an application event. Events may be framework events (such as RequestHandledEvent) or application-specific events.
     *
     * @param event the event to publish
     * @see RequestHandledEvent
     */
    public static void publishEvent(ApplicationEvent event) {
        _checkContextInitialized();
        CONTEXT.publishEvent(event);
    }
}
